﻿/*
 * This file is part of MonoStrategy.
 *
 * Copyright (C) 2010-2011 Christoph Husse
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: 
 *      # Christoph Husse
 * 
 * Also checkout our homepage: http://monostrategy.codeplex.com/
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MonoStrategy
{
    public delegate void DChangeHandler<in TObject, in TValue>(TObject inSender, TValue inOldValue, TValue inNewValue);
    public delegate void DNotifyHandler<in TObject>(TObject inSender);
    public delegate void DNotifyHandler<in TObject, in TArg1>(TObject inSender, TArg1 inArg1);
    public delegate void DNotifyHandler<in TObject, in TArg1, TArg2>(TObject inSender, TArg1 inArg1, TArg2 inArg2);
    public delegate void DNotifyHandler<in TObject, in TArg1, TArg2, TArg3>(TObject inSender, TArg1 inArg1, TArg2 inArg2, TArg3 inArg3);

    /// <summary>
    /// A topological list is meant to provide a highly optimized way of doing jobs like
    /// "give me all resources around XY sorted ascendingly by distance to XY". Since this
    /// is a very important and frequently used operation in the game, its worth to create
    /// a special list for it. Please be aware of that this list only ensurs sorting by
    /// distance in an approximative way, based on the granularity given. If you use 1 as
    /// granularity, which would definitely be stupid, you would get exact sorting by distance.
    /// If you choose 10, the same task will have an error of 10 length units on average, which
    /// doesn't matter at all for this game, but exponentially speeds up performance.
    /// </summary>
    /// <typeparam name="TValue"></typeparam>
    public class TopologicalList<TValue> where TValue : IPositionTracker
    {
        private List<List<List<TValue>>> m_Topology = new List<List<List<TValue>>>();
        private List<TValue> m_Entries = new List<TValue>();

        public Int32 Granularity { get; private set; }
        public Int32 Width { get; private set; }
        public Int32 Height { get; private set; }

        public TopologicalList(int inGranularity, int inWidth, int inHeight)
        {
            Granularity = inGranularity;
            Width = inWidth / inGranularity + 1;
            Height = inHeight / inGranularity + 1;

            for (int x = 0; x < Width; x++)
            {
                List<List<TValue>> column = new List<List<TValue>>();

                m_Topology.Add(column);

                for (int y = 0; y < Height; y++)
                {
                    column.Add(new List<TValue>());
                }
            }
        }

        public void ForEach(Func<TValue, bool> inHandler)
        {
            lock (m_Topology)
            {
                foreach (var entry in m_Entries)
                {
                    if (!inHandler(entry))
                        break;
                }
            }
        }

        public void CopyArea(Rectangle inArea, List<TValue> outEntries)
        {
            lock (m_Topology)
            {
                int granX1 = inArea.X / Granularity;
                int granY1 = inArea.Y / Granularity;
                int granX2 = (inArea.X + inArea.Width + Granularity - 1) / Granularity;
                int granY2 = (inArea.Y + inArea.Height + Granularity - 1) / Granularity;

                for (int x = granX1; x < granX2; x++)
                {
                    for (int y = granY1; y < granY2; y++)
                    {
                        outEntries.AddRange(m_Topology[x][y]);
                    }
                }
            }
        }

        public IEnumerable<TValue> EnumAt(Point inAtCell)
        {
            lock (m_Topology)
            {
                List<TValue> list = OpenList(CyclePoint.FromGrid(inAtCell));
                List<TValue> result = new List<TValue>();

                foreach (var e in list)
                {
                    if ((e.Position.XGrid == inAtCell.X) && (e.Position.YGrid == inAtCell.Y))
                        result.Add(e);
                }

                return result;
            }
        }

        /// <summary>
        /// If you want to limit the radius of <see cref="EnumAround"/>, it is recommended using this function,
        /// since it is not obvious how to archieve this from outside the topological list!
        /// </summary>
        public WalkResult EnumAround(Point inAround, int inRadius, Func<TValue, WalkResult> inHandler)
        {
            lock (m_Topology)
            {
                int topRadius = (int)(((long)inRadius + Granularity - 1) / Granularity);
                Point topAround = new Point(inAround.X / Granularity, inAround.Y / Granularity);

                return GridSearch.GridWalkAround(topAround, Width, Height, (position) =>
                {
                    if ((Math.Abs(position.X - topAround.X) > topRadius) || (Math.Abs(position.Y - topAround.Y) > topRadius))
                        return WalkResult.Abort;

                    foreach (var entry in m_Topology[position.X][position.Y])
                    {
                        if ((Math.Abs(entry.Position.X - inAround.X) > inRadius) || (Math.Abs(entry.Position.Y - inAround.Y) > inRadius))
                            return WalkResult.NotFound;

                        if (inHandler(entry) == WalkResult.Success)
                            return WalkResult.Success;
                    }

                    return WalkResult.NotFound;
                });
            }
        }

        public WalkResult EnumAround(Point inAround, Func<TValue, WalkResult> inHandler)
        {
            lock (m_Topology)
            {
                return GridSearch.GridWalkAround(new Point(inAround.X / Granularity, inAround.Y / Granularity), Width, Height, (position) =>
                {
                    foreach (var entry in m_Topology[position.X][position.Y])
                    {
                        if (inHandler(entry) == WalkResult.Success)
                            return WalkResult.Success;
                    }

                    return WalkResult.NotFound;
                });
            }
        }        

        protected List<TValue> OpenList(CyclePoint inPosition)
        {
            int granX = inPosition.XGrid / Granularity;
            int granY = inPosition.YGrid / Granularity;

            return m_Topology[granX][granY];
        }

        public void Add(TValue inValue)
        {
            lock (m_Topology)
            {
                List<TValue> list = OpenList(inValue.Position);

                if (list.Contains(inValue))
                    throw new InvalidOperationException("Value has already been added.");

                inValue.OnPositionChanged += OnPositionChanged;
                list.Add(inValue);
                m_Entries.Add(inValue);
            }
        }

        private void OnPositionChanged(IPositionTracker inTrackable, CyclePoint inOldPosition, CyclePoint inNewPosition)
        {
            lock (m_Topology)
            {
                List<TValue> oldList = OpenList(inOldPosition);
                List<TValue> newList = OpenList(inNewPosition);

                if (oldList == newList)
                    return;

                if (!oldList.Contains((TValue)inTrackable))
                    throw new ApplicationException();

                if (newList.Contains((TValue)inTrackable))
                    throw new ApplicationException();

                oldList.Remove((TValue)inTrackable);
                newList.Add((TValue)inTrackable);
            }
        }

        public void Remove(TValue inValue)
        {
            lock (m_Topology)
            {
                List<TValue> list = OpenList(inValue.Position);

                inValue.OnPositionChanged -= OnPositionChanged;

                if (!list.Remove(inValue) || !m_Entries.Remove(inValue))
                    throw new ApplicationException();
            }
        }
    }
}
